using System;
using System.Collections.Generic;
using System.Linq;
using Godot;

namespace Arcane.Source.Combat
{
    public class RuneMatch
    {
        public event Action<RuneData, int, int> OnRuneUpdate;
        public event Action<float> OnTimeScaleChange;
        public event Action<int> OnEliminate;
        public event Action<int, int, int, int> OnRuneEliminate;
        public event Action<int, int, int, int> OnUpgrade;
        public event Action<int, int, int, int> ShowHint;

        private readonly RuneData[,] runes;
        private readonly int typeCount;

        private readonly Queue<RuneData>[] spawners;

        private readonly List<(int, int)> _settledCache = [];

        private float timeScale = 1;

        public int Col => runes.GetLength(0);
        public int Row => runes.GetLength(1);
        public float TimeScale
        {
            get => timeScale;
            set
            {
                timeScale = value;
                OnTimeScaleChange?.Invoke(timeScale);
            }
        }
        public int RuneDropFrame { get; set; } = 8;
        public int DropCheckFrame { get; set; } = 2;

        public RuneData this[int col, int row] => IsEmpty(col, row) ? null : runes[col, row];

        private float checkTimer = 0;
        private bool _prevIsStatic = false;

        private int hintDelayTick = 0;
        private int? hintCol, hintRow, hintToCol, hintToRow;

        public RuneMatch(int col, int row, int typeCount)
        {
            runes = new RuneData[col, row];
            spawners = Enumerable.Repeat(new Queue<RuneData>(), col).ToArray();
            this.typeCount = typeCount;
        }

        public void Setup()
        {
            var col = Col;
            var row = Row;
            for (int r = 0; r < row; r++)
            {
                for (int c = 0; c < col; c++)
                {
                    var left = GetRuneType(c - 1, r);
                    var up = GetRuneType(c, r - 1);
                    var randPool = Enumerable
                        .Range(0, typeCount)
                        .Where(i => i != left && i != up)
                        .ToArray();

                    var rune = new RuneData();
                    rune.Setup(randPool[GD.Randi() % randPool.Length]);
                    runes[c, r] = rune;

                    RequestSync(c, r);
                }
            }
        }

        public void Tick()
        {
            DoRespawn();
            UpdateRunes();
        }

        private void DoRespawn()
        {
            for (int c = 0; c < spawners.Length; c++)
            {
                if (spawners[c].Count > 0 && IsEmpty(c, 0))
                {
                    var rune = spawners[c].Dequeue();
                    rune.Setup((int)(GD.Randi() % typeCount));
                    runes[c, 0] = rune;
                    RequestSync(c, 0);
                }
            }
        }

        private void UpdateRunes()
        {
            bool isStatic = true;
            var (col, row) = (Col, Row);
            for (int r = 0; r < row; r++)
            {
                for (int c = 0; c < col; c++)
                {
                    if (runes[c, r] == null)
                    {
                        isStatic = false;
                        continue;
                    }

                    if (runes[c, r].IsDroping())
                    {
                        isStatic = false;
                        if (runes[c, r].timer >= RuneDropFrame)
                        {
                            if (IsSolid(c, r + 1))
                            {
                                runes[c, r].SetIdle();
                                RequestSync(c, r);
                                _settledCache.Add((c, r));
                            }
                            else if (IsEmpty(c, r + 1))
                            {
                                // Move down.
                                runes[c, r + 1] = runes[c, r];
                                runes[c, r] = null;

                                runes[c, r + 1].timer = 0;
                                RequestSync(c, r);
                                RequestSync(c, r + 1);
                            }
                        }
                        else
                        {
                            runes[c, r].timer += TimeScale;
                            //RequestSync(c, r);
                        }
                    }
                    else if (checkTimer > DropCheckFrame && runes[c, r].IsIdle())
                    {
                        if (IsEmpty(c, r + 1))
                        {
                            isStatic = false;
                            runes[c, r + 1] = runes[c, r];
                            runes[c, r] = null;

                            runes[c, r + 1].SetDrop();
                            RequestSync(c, r);
                            RequestSync(c, r + 1);
                        }

                    }
                }
            }

            if (isStatic && !_prevIsStatic)
            {
                if (!DoEliminate([.. _settledCache]))
                {
                    hintDelayTick = Rule.HINT_TICK;
                    hintCol = hintRow = hintToCol = hintToRow = null;
                }
                _settledCache.Clear();
            }


            if (hintDelayTick > 0)
            {
                hintDelayTick -= 1;
                if (hintDelayTick <= 0)
                {
                    if (hintCol == null || hintRow == null || hintToCol == null || hintToRow == null)
                    {
                        if (CheckEliminate() is var (c, r, tc, tr))
                        {
                            hintCol = c;
                            hintRow = r;
                            hintToCol = tc;
                            hintToRow = tr;
                        }
                        else
                        {
                            (int, int, int, int)? p;
                            do
                            {
                                Reshuffle();
                            }
                            while (!(p = CheckEliminate()).HasValue);

                            var (pc, pr, ptc, ptr) = p.Value;

                            hintCol = pc;
                            hintRow = pr;
                            hintToCol = ptc;
                            hintToRow = ptr;
                            SyncAll();
                        }
                    }
                    ShowHint?.Invoke(hintCol.Value, hintRow.Value, hintToCol.Value, hintToRow.Value);
                    hintDelayTick = Rule.HINT_TICK / 2;
                }
            }

            if (checkTimer > DropCheckFrame)
            {
                checkTimer = 0;
            }
            else
            {
                checkTimer += TimeScale;
            }

            _prevIsStatic = isStatic;
        }

        public bool DoEliminate(params (int, int)[] trigPositions)
        {
            var (col, row) = (Col, Row);
            var dp = new (int h, int v, int? t)[col];
            var check = new bool[col, row];
            var counter = new List<int>();
            var counterIndices = new int?[col, row];
            var success = false;

            for (int r = 0; r < row; r++)
            {
                for (int c = 0; c < col; c++)
                {
                    var t = GetEliminatableType(c, r);
                    if (t == null)
                    {
                        dp[c] = (0, 0, null);
                        continue;
                    }

                    var h = 0;
                    var v = 0;

                    if (ColValid(c - 1) && t == dp[c - 1].t)
                    {
                        h = dp[c - 1].h + 1;
                    }
                    if (t == dp[c].t)
                    {
                        v = dp[c].v + 1;
                    }

                    if (h == 2)
                    {
                        success = true;
                        int? scoreIndex = null;
                        int bonus = 0;
                        for (int i = c - h; i <= c; i++)
                        {
                            check[i, r] = true;
                            if (counterIndices[i, r] is int idx)
                            {
                                scoreIndex = idx;
                            }
                            else
                            {
                                bonus += 1;
                            }
                        }

                        if (scoreIndex == null)
                        {
                            scoreIndex = counter.Count;
                            counter.Add(3);
                        }
                        else
                        {
                            counter[scoreIndex.Value] += bonus;
                        }
                        for (int i = c - h; i <= c; i++) counterIndices[i, r] = scoreIndex;
                    }
                    else if (h > 2)
                    {
                        success = true;
                        check[c, r] = true;
                        counterIndices[c, r] = counterIndices[c - 1, r];
                        counter[counterIndices[c, r].Value] += 1;
                    }

                    if (v == 2)
                    {
                        success = true;
                        int? scoreIndex = null;
                        int bonus = 0;
                        for (int i = r - v; i <= r; i++)
                        {
                            check[c, i] = true;
                            if (counterIndices[c, i] is int idx)
                            {
                                scoreIndex = idx;
                            }
                            else
                            {
                                bonus += 1;
                            }
                        }

                        if (scoreIndex == null)
                        {
                            scoreIndex = counter.Count;
                            counter.Add(3);
                        }
                        else
                        {
                            counter[scoreIndex.Value] += bonus;
                        }
                        for (int i = r - v; i <= r; i++) counterIndices[c, i] = scoreIndex;
                    }
                    else if (v > 2)
                    {
                        success = true;
                        check[c, r] = true;
                        counterIndices[c, r] = counterIndices[c, r - 1];
                        if (ColValid(c - 1) && counterIndices[c - 1, r] is int idx)
                        {
                            counter[counterIndices[c, r].Value] += counter[idx];
                            for (int i = c - dp[c - 1].h; i <= c; i++) counterIndices[i, r] = counterIndices[c, r];
                        }
                        counter[counterIndices[c, r].Value] += 1;
                    }
                    dp[c] = (h, v, t);
                }
            }

            if (success)
            {
                var counted = Enumerable.Repeat(false, counter.Count).ToArray();
                // Handle combination
                foreach (var (c, r) in trigPositions)
                {
                    if (counterIndices[c, r] is int idx && !counted[idx])
                    {
                        var count = counter[idx];
                        counted[idx] = true;

                        if (count <= 3) continue;

                        check[c, r] = false;

                        Upgrade(c, r, Mathf.Clamp(count - 3, 0, int.MaxValue));
                    }
                }

                // Do eliminate and count score.
                var score = 0;
                for (int r = 0; r < row; r++)
                {
                    for (int c = 0; c < col; c++)
                    {
                        if (check[c, r])
                        {
                            score += runes[c, r].level + 1;
                            Remove(c, r);
                        }
                    }
                }
                OnEliminate?.Invoke(score);
            }

            return success;
        }

        public bool TrySwap(int c1, int r1, int c2, int r2)
        {
            (runes[c1, r1], runes[c2, r2]) = (runes[c2, r2], runes[c1, r1]);
            if (!DoEliminate((c1, r1), (c2, r2)))
            {
                (runes[c1, r1], runes[c2, r2]) = (runes[c2, r2], runes[c1, r1]);
                return false;
            }

            RequestSync(c1, r1);
            RequestSync(c2, r2);
            return true;
        }

        public void Remove(int col, int row)
        {
            if (runes[col, row] == null) return;
            var r = runes[col, row];
            OnRuneEliminate?.Invoke(r.type, r.level, col, row);

            spawners[col].Enqueue(runes[col, row]);
            runes[col, row] = null;

            RequestSync(col, row);
        }

        public void Upgrade(int col, int row, int level)
        {
            if (IsEmpty(col, row)) return;
            var rune = runes[col, row];
            var delta = level + rune.level - 2;

            if (delta > 0) OnRuneEliminate?.Invoke(rune.type, delta, col, row);

            rune.level = Mathf.Clamp(rune.level + level, 0, 2);
            
            OnUpgrade?.Invoke(rune.type, rune.level, col, row);
            RequestSync(col, row);
        }

        public void SetRuneType(int col, int row, int ty)
        {
            if (IsEmpty(col, row)) return;
            runes[col, row].type = ty;
            RequestSync(col, row);
        }

        private void RequestSync(int col, int row) => OnRuneUpdate?.Invoke(runes[col, row], col, row);

        private void SyncAll()
        {
            foreach (var r in Enumerable.Range(0, Row))
            {
                foreach (var c in Enumerable.Range(0, Col))
                {
                    RequestSync(c, r);
                }
            }
        }

        public int? GetRuneType(int col, int row)
        {
            if (!ColValid(col) || !RowValid(row)) return null;
            return runes[col, row]?.type;
        }

        public int? GetEliminatableType(int col, int row)
        {
            if (!IsEliminatable(col, row)) return null;
            return runes[col, row].type;
        }

        public bool CheckCol(int col)
        {
            int type = runes[col, 0].type;
            int cnt = 1;
            for (int r = 1; r < Row; r++)
            {
                int curType = runes[col, r].type;

                if (curType == type)
                {
                    cnt += 1;
                    if (cnt >= 3) return true;
                }
                else
                {
                    type = curType;
                    cnt = 1;
                }
            }
            return false;
        }

        public bool CheckRow(int row)
        {
            int type = runes[0, row].type;
            int cnt = 1;
            for (int c = 1; c < Col; c++)
            {
                int curType = runes[c, row].type;
                if (curType == type)
                {
                    cnt += 1;
                    if (cnt >= 3) return true;
                }
                else
                {
                    type = curType;
                    cnt = 1;
                }
            }
            return false;
        }

        public (int, int, int, int)? CheckEliminate()
        {
            foreach (var r in Enumerable.Range(0, Row))
            {
                foreach (var c in Enumerable.Range(0, Col))
                {
                    if (ColValid(c + 1))
                    {
                        bool acc = false;
                        Swap(c, r, c + 1, r);
                        acc = acc || CheckCol(c);
                        acc = acc || CheckCol(c + 1);
                        acc = acc || CheckRow(r);
                        Swap(c, r, c + 1, r);

                        if (acc) return (c, r, c + 1, r);
                    }

                    if (RowValid(r + 1))
                    {
                        bool acc = false;
                        Swap(c, r, c, r + 1);
                        acc = acc || CheckRow(r);
                        acc = acc || CheckRow(r + 1);
                        acc = acc || CheckCol(c);
                        Swap(c, r, c, r + 1);

                        if (acc) return (c, r, c, r + 1);
                    }
                }
            }
            return null;
        }

        public void Swap(int c1, int r1, int c2, int r2)
        {
            (runes[c1, r1], runes[c2, r2]) = (runes[c2, r2], runes[c1, r1]);
        }

        private IEnumerable<(int, int)> GetPoints()
        {
            foreach (var r in Enumerable.Range(0, Row))
            {
                foreach (var c in Enumerable.Range(0, Col))
                {
                    yield return (c, r);
                }
            }
        }

        public void Reshuffle()
        {
            var points = GetPoints().Where(_ => GD.Randi() % 2 == 0);
            foreach (var (c, r) in points)
            {
                List<byte> flag = [];
                if (ColValid(c + 1)) flag.Add(1);
                if (RowValid(r + 1)) flag.Add(2);
                if (ColValid(c - 1)) flag.Add(3);
                if (RowValid(r - 1)) flag.Add(4);

                switch (flag[(int)(GD.Randi() % flag.Count)])
                {
                    case 1:
                        Swap(c, r, c + 1, r);
                        break;
                    case 2:
                        Swap(c, r, c, r + 1);
                        break;
                    case 3:
                        Swap(c, r, c - 1, r);
                        break;
                    case 4:
                        Swap(c, r, c, r - 1);
                        break;
                }
            }
        }

        public bool ColValid(int col) => col >= 0 && col < runes.GetLength(0);
        public bool RowValid(int row) => row >= 0 && row < runes.GetLength(1);
        public bool IsEliminatable(int col, int row) => ColValid(col) && RowValid(row) && runes[col, row] is RuneData d && d.IsIdle();
        public bool IsSolid(int col, int row)
        {
            if (!ColValid(col) || !RowValid(row)) return true;
            return runes[col, row]?.IsIdle() == true;
        }
        public bool IsEmpty(int col, int row)
        {
            if (!ColValid(col) || !RowValid(row)) return false;
            return runes[col, row] == null;
        }
    }
}